import { CSSProperties, dom, EventHandler, Nodes } from './index';

export type JSXProps<T extends Nodes> = Record<string, unknown> & {
	[key in keyof HTMLElementEventMap as `on${key}`]?:
		| ((this: T, event: HTMLElementEventMap[key], target: T) => void)
		| string;
} & {
	style?: string | CSSProperties;
	class?: string | string[];
	ref?: (elem: T) => void;
};

// export type JSXChildren = (string | boolean | number | Nodes)[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type JSXChildren = any[];

export interface JSXInit {
	initialize?(children: (Nodes | Text)[]): this;
}

export type JSXExtNodes = Nodes & JSXInit;

function expect(what: string, found: string): never {
	throw new Error(`invalid css token: expect ${what}, but found: ${found}'`);
}

function parseCss(str: string): CSSProperties {
	const properties: Record<string, string> = {};
	const regex =
		/((?<=(?:^|;)\s*)(?:--[^:]+|[a-z]+(?:-[a-z]+)*)(?=\s*:))|(\s*:\s*)|(\s*;\s*)|((?<=:\s*)(?:'[^']*(?<!')'|"[^"]*(?<!")"|[^;:'"]+)+)|([^:]+)/g;
	let m: RegExpExecArray | null = null;
	let key: string | undefined;
	let value: string | undefined;
	let state = 0;
	while ((m = regex.exec(str))) {
		// key - 1
		// : - 2
		// ; - 3
		// value - 4
		const word = m[0];
		const type = m.indexOf(word, 1);
		if (state === 0) {
			if (type === 1) {
				if (word.startsWith('--')) {
					key = word;
				} else {
					key = word.replace(/-([a-z])/g, (s, c) => c.toUpperCase());
				}
				state = 1;
			} else if (type === 3) {
				// do nothing
			} else expect('property name', word);
		} else if (state === 1) {
			if (type !== 2) expect('":"', word);
			state = 2;
		} else if (state === 2) {
			if (type === 4) {
				value = word;
				if (key) {
					properties[key] = value;
				}
			} else if (type === 3) {
				// do nothing
			} else {
				expect('css value', word);
			}
			state = 0;
		}
	}
	return properties;
}

function organizeChildren(children: JSXChildren): (Nodes | Text)[] {
	if (children instanceof Nodes) return [children];
	return children
		.map((c) => {
			if (c instanceof Nodes) {
				return [c];
			} else if (c instanceof Array) {
				return c;
			} else {
				return [c];
			}
		})
		.flat(1)
		.map((c) => (c instanceof Nodes ? c : dom.text(c + '')));
}

export function jsxGen<T extends JSXExtNodes>(
	elem: T,
	props: JSXProps<T>,
	children: JSXChildren,
	interceptor?: (attr: string, value: unknown) => boolean
) {
	if (children) {
		if (typeof elem.initialize === 'function') {
			elem.initialize(organizeChildren(children));
		} else if (children.length > 0) {
			elem.appendAll(organizeChildren(children));
		}
	}
	Object.keys(props).forEach((k) => {
		const v = props[k];
		if (interceptor?.(k, v) !== true) {
			if (k.startsWith('on')) {
				if (typeof v === 'function') {
					elem.on(k.substring(2) as keyof HTMLElementEventMap, v as EventHandler<keyof HTMLElementEventMap, T>);
				} else if (typeof v === 'string') {
					elem.attr(k, v);
				}
			} else if (k === 'style') {
				if (v && typeof v === 'string') {
					elem.css(parseCss(v));
				} else if (v && v instanceof Object) {
					elem.css(v);
				}
			} else if (k === 'class') {
				if (typeof v === 'string' || v instanceof Array) {
					elem.addClass(...([v].flat().filter((v) => typeof v === 'string') as string[]));
				}
			} else if (k === 'ref') {
				//
			} else if (typeof v === 'string' || typeof v === 'boolean' || v === null) {
				elem.attr(k, v);
			} else if (typeof v === 'number') {
				elem.attr(k, v + '');
			}
		}
	});
}

export interface JSXFunction<T extends Nodes> {
	(attrs: JSXProps<T>, children: JSXChildren): T;
}

export interface JSXClass<T extends Nodes> {
	new (attrs: JSXProps<T>, children: JSXChildren): T;
}

const funcProto = Object.getPrototypeOf(Function);

function isComponentClass<T extends Nodes>(func: unknown): func is JSXClass<T> {
	let proto = Object.getPrototypeOf(func);
	while (proto !== funcProto) {
		if (proto === Nodes) {
			return true;
		}
		proto = Object.getPrototypeOf(proto);
	}
	return false;
}

export function h<T extends Nodes>(
	tag: JSXFunction<T> | JSXClass<T> | keyof HTMLElementTagNameMap,
	attrs?: JSXProps<T> | null,
	...children: JSXChildren[]
): T {
	attrs = attrs || {};
	let elm: T;
	if (typeof tag === 'function') {
		if (isComponentClass(tag)) {
			elm = new tag(attrs, children ?? []);
		} else {
			elm = tag(attrs, children ?? []);
		}
		if (attrs.ref) {
			elm.ref(attrs.ref);
		}
	} else {
		elm = dom.new(tag) as T;
		if (attrs.ref) {
			elm.ref(attrs.ref);
		}
		jsxGen(elm, attrs, children ?? []);
	}
	return elm as T;
}

export function fragment<T extends Nodes>(_: JSXProps<T>, children: JSXChildren[]): Nodes {
	return new Nodes(organizeChildren(children).flat(1));
}

export type JSXNode = JSXProps<Nodes>;
type ExcludeAttr = keyof HTMLElement | 'style';
export type JSXElement<T> = JSXNode & Partial<Omit<T, ExcludeAttr>>;

/**
 * Convenient to construct a custom component
 */
export abstract class JSXComponent<T extends JSXComponent<T>> extends Nodes {
	constructor(root: Iterable<Element | Text>, attrs?: JSXProps<T>, children?: JSXChildren) {
		super(root);
		jsxGen(this as unknown as T, attrs ?? {}, children ?? []);
	}
}

export abstract class Component<T extends Component<T, P>, P> extends Nodes {
	constructor(root: Iterable<Element | Text>, attrs?: JSXProps<T> & P, children?: JSXChildren) {
		super(root);
		jsxGen(this as unknown as T, attrs ?? {}, children ?? []);
	}
}

declare global {
	// eslint-disable-next-line @typescript-eslint/no-namespace
	namespace JSX {
		// type IntrinsicAttributes = JSXAttributes<any>
		interface IntrinsicElements {
			a: JSXElement<HTMLAnchorElement>;
			abbr: JSXElement<HTMLElement>;
			address: JSXElement<HTMLElement>;
			area: JSXElement<HTMLAreaElement>;
			article: JSXElement<HTMLElement>;
			aside: JSXElement<HTMLElement>;
			audio: JSXElement<HTMLAudioElement>;
			b: JSXElement<HTMLElement>;
			base: JSXElement<HTMLBaseElement>;
			bdi: JSXElement<HTMLElement>;
			bdo: JSXElement<HTMLElement>;
			blockquote: JSXElement<HTMLQuoteElement>;
			body: JSXElement<HTMLBodyElement>;
			br: JSXElement<HTMLBRElement>;
			button: JSXElement<HTMLButtonElement>;
			canvas: JSXElement<HTMLCanvasElement>;
			caption: JSXElement<HTMLTableCaptionElement>;
			cite: JSXElement<HTMLElement>;
			code: JSXElement<HTMLElement>;
			col: JSXElement<HTMLTableColElement>;
			colgroup: JSXElement<HTMLTableColElement>;
			data: JSXElement<HTMLDataElement>;
			datalist: JSXElement<HTMLDataListElement>;
			dd: JSXElement<HTMLElement>;
			del: JSXElement<HTMLModElement>;
			details: JSXElement<HTMLDetailsElement>;
			dfn: JSXElement<HTMLElement>;
			dialog: JSXElement<HTMLDialogElement>;
			div: JSXElement<HTMLDivElement>;
			dl: JSXElement<HTMLDListElement>;
			dt: JSXElement<HTMLElement>;
			em: JSXElement<HTMLElement>;
			embed: JSXElement<HTMLEmbedElement>;
			fieldset: JSXElement<HTMLFieldSetElement>;
			figcaption: JSXElement<HTMLElement>;
			figure: JSXElement<HTMLElement>;
			footer: JSXElement<HTMLElement>;
			form: JSXElement<HTMLFormElement>;
			h1: JSXElement<HTMLHeadingElement>;
			h2: JSXElement<HTMLHeadingElement>;
			h3: JSXElement<HTMLHeadingElement>;
			h4: JSXElement<HTMLHeadingElement>;
			h5: JSXElement<HTMLHeadingElement>;
			h6: JSXElement<HTMLHeadingElement>;
			head: JSXElement<HTMLHeadElement>;
			header: JSXElement<HTMLElement>;
			hgroup: JSXElement<HTMLElement>;
			hr: JSXElement<HTMLHRElement>;
			html: JSXElement<HTMLHtmlElement>;
			i: JSXElement<HTMLElement>;
			iframe: JSXElement<HTMLIFrameElement>;
			img: JSXElement<HTMLImageElement>;
			input: JSXElement<HTMLInputElement>;
			ins: JSXElement<HTMLModElement>;
			kbd: JSXElement<HTMLElement>;
			label: JSXElement<HTMLLabelElement>;
			legend: JSXElement<HTMLLegendElement>;
			li: JSXElement<HTMLLIElement>;
			link: JSXElement<HTMLLinkElement>;
			main: JSXElement<HTMLElement>;
			map: JSXElement<HTMLMapElement>;
			mark: JSXElement<HTMLElement>;
			menu: JSXElement<HTMLMenuElement>;
			meta: JSXElement<HTMLMetaElement>;
			meter: JSXElement<HTMLMeterElement>;
			nav: JSXElement<HTMLElement>;
			noscript: JSXElement<HTMLElement>;
			object: JSXElement<HTMLObjectElement>;
			ol: JSXElement<HTMLOListElement>;
			optgroup: JSXElement<HTMLOptGroupElement>;
			option: JSXElement<HTMLOptionElement>;
			output: JSXElement<HTMLOutputElement>;
			p: JSXElement<HTMLParagraphElement>;
			picture: JSXElement<HTMLPictureElement>;
			pre: JSXElement<HTMLPreElement>;
			progress: JSXElement<HTMLProgressElement>;
			q: JSXElement<HTMLQuoteElement>;
			rp: JSXElement<HTMLElement>;
			rt: JSXElement<HTMLElement>;
			ruby: JSXElement<HTMLElement>;
			s: JSXElement<HTMLElement>;
			samp: JSXElement<HTMLElement>;
			script: JSXElement<HTMLScriptElement>;
			section: JSXElement<HTMLElement>;
			select: JSXElement<HTMLSelectElement>;
			slot: JSXElement<HTMLSlotElement>;
			small: JSXElement<HTMLElement>;
			source: JSXElement<HTMLSourceElement>;
			span: JSXElement<HTMLSpanElement>;
			strong: JSXElement<HTMLElement>;
			style: JSXElement<HTMLStyleElement>;
			sub: JSXElement<HTMLElement>;
			summary: JSXElement<HTMLElement>;
			sup: JSXElement<HTMLElement>;
			table: JSXElement<HTMLTableElement>;
			tbody: JSXElement<HTMLTableSectionElement>;
			td: JSXElement<HTMLTableCellElement>;
			template: JSXElement<HTMLTemplateElement>;
			textarea: JSXElement<HTMLTextAreaElement>;
			tfoot: JSXElement<HTMLTableSectionElement>;
			th: JSXElement<HTMLTableCellElement>;
			thead: JSXElement<HTMLTableSectionElement>;
			time: JSXElement<HTMLTimeElement>;
			title: JSXElement<HTMLTitleElement>;
			tr: JSXElement<HTMLTableRowElement>;
			track: JSXElement<HTMLTrackElement>;
			u: JSXElement<HTMLElement>;
			ul: JSXElement<HTMLUListElement>;
			var: JSXElement<HTMLElement>;
			video: JSXElement<HTMLVideoElement>;
			wbr: JSXElement<HTMLElement>;
		}

		interface IntrinsicElements {
			[key: string]: JSXElement<HTMLElement>;
		}
		type Element = Nodes;
		type ElementClass = JSXFunction<Nodes> | Nodes;
	}
}
